import {
    AfterViewInit,
    AfterContentInit,
    Component,
    ContentChild,
    ContentChildren,
    EventEmitter,
    Input,
    Output,
    OnInit,
    OnChanges,
    QueryList,
    SimpleChanges,
    ViewChild,
    OnDestroy
} from '@angular/core';

import { takeUntil } from 'rxjs/operators';

import { DxDataGridComponent, DxFilterBuilderComponent } from 'devextreme-angular';
import { locale, loadMessages } from 'devextreme/localization';
import DxDataGrid from 'devextreme/ui/data_grid';

// We have to add "resolveJsonModule": true in tsconfig.json to makes this code work.
import * as zhMessages from '../../../../../../node_modules/devextreme/localization/messages/zh.json';

import { BaseDirective } from '../../../../core/models/base-directive.model';
import { DataGridConfig } from '../../../../core/config/data-grid.config';
import { DataColumn } from '../shared/models/data-column.model.js';
import { DataGridPageChangedEventArg } from '../shared/models/data-grid-page-changed-event-arg.model';
import { DataGridSelectionChangedEventArg } from '../shared/models/data-grid-selection-changed-event-arg.model';
import { FilterChangedEventArg } from '../shared/models/filter-changed-event-arg.model.js';
import { DataGridColumnComponent } from './data-grid-column.component';
import { DataGridMasterDetailComponent } from './data-grid-master-detail.component';

declare const $: any;

/**
 * Wrapper of DevExtreme DataGrid. Other components with the same selector can replace its functionality.
 */
@Component({
    selector: 'op-data-grid',
    templateUrl: './data-grid.component.html',
    styleUrls: ['./data-grid.component.scss']
})
export class DataGridComponent extends BaseDirective implements AfterViewInit, AfterContentInit, OnInit, OnChanges, OnDestroy {
    // The DataGridColumnComponent list used to generate the data columns.
    @ContentChildren(DataGridColumnComponent) columns: QueryList<DataGridColumnComponent>;

    // DataGridMasterDetailComponent used for mater-detail view.
    @ContentChild(DataGridMasterDetailComponent, { static: false }) masterDetail: DataGridMasterDetailComponent;

    // DxDataGridComponent used for setting the datasource or any other properties programmatically.
    @ViewChild('grid', { static: false }) grid: DxDataGridComponent;

    @ViewChild(DxFilterBuilderComponent, { static: false }) filterBuilder: DxFilterBuilderComponent;

    // Paginator for access its changePage or any other methods.
    @ViewChild('paginator', { static: false }) paginator: any;

    // pageChanged event provides DataGridPageChangedEventArg to event hanlder.
    @Output() pageChanged: EventEmitter<DataGridPageChangedEventArg> = new EventEmitter<DataGridPageChangedEventArg>();

    // selectionChanged event provides DataGridSelectionChangedEventArg to event hanlder.
    @Output() selectionChanged: EventEmitter<DataGridSelectionChangedEventArg> = new EventEmitter<DataGridSelectionChangedEventArg>();

    @Output() filterChanged: EventEmitter<FilterChangedEventArg> = new EventEmitter<FilterChangedEventArg>();

    // Datasource for DataGrid and will be set programmatically due to server side paging or client side paging needs.
    @Input() dataSource: any[];

    // If the needFilterBuilder is true this will be used to extract the data type by the data column name.
    @Input() dataTypeListForFilterBuilder: DataColumn[];

    // This is for the filter picker of filter builder.
    // Sometimes we need to exclude some columns with specific data types to be displayed in the filter picker.
    // For example, the column with cellTemplate. But we cannot just exculde all columns with cellTemplate because it may only contain the pipe to format the text.
    // Something like <span>{{lastUpdatedDate | date: 'yyyy-MM-dd'}}</span>
    // Some columns with cellTempate which contains button, link will be excluded.
    // That's why we need to know which columns we need to exclude from filter picker.
    // The default value is [].
    @Input() filterExcludedDataTypes: string[];

    @Input() filterBuilderBackgroundColor: string;

    // The data binding key name for DataGrid.
    @Input() keyName: string;

    @Input() enablePaging: boolean;
    @Input() enableMutipleSelection: boolean;
    @Input() allowColumnResizing: boolean;
    @Input() hoverStateEnabled: boolean;
    @Input() rowAlternationEnabled: boolean;
    @Input() needFilterBuilder: boolean;
    @Input() columnAutoWidth: boolean;

    // For the paginator.
    @Input() totalRowsCount: number;

    // For the paginator. How many items will be displayed on one page.
    @Input() pageSize: number;

    // For the paginator. Array of integer values to display inside rows per page dropdown.
    @Input() pageSizeOptions: number[];

    // For the paginator. Number of page links to display.
    @Input() pageLinkSize: number;

    // If cureent paging is client side. The default value is false.
    isClientSidePaging: boolean;

    // The DataGrid column's min width.
    columnMinWidth: number;

    // Current page index for paging.
    currentPage: number;

    // For the paginator.
    totalPageCount: number;

    // All selected row keys and will be emitted via selectionChanged event.
    selectedRowKeys: any[];

    // 本地语言类型
    locale: string;

    // For filter builder.
    filterExpression: any;

    /**
     * Gets the current data grid instance for specific useage.
     */
    get gridInstance(): DxDataGrid {
        return this.grid.instance;
    }

    /**
     * Gets the current data grid component for specific useage.
     */
    get gridComponent(): DxDataGridComponent {
        return this.grid;
    }

    constructor() {
        super();
    }

    ngOnInit(): void {
        super.ngOnInit();

        this.loadLocalization();
        this.initializeProperties();
    }

    ngAfterViewInit(): void {
        // Angular team recommended retrieving query results in ngAfterViewInit for view queries and ngAfterContentInit for content queries. 
        // This is because by the time those lifecycle hooks run, change detection has completed for the relevant nodes and we can guarantee 
        // that we have collected all the possible query results.

        // Initial value assignment.
        this.setFilterBuilderFields();

        // Some observable types will not give you its current value, like Subject type.
        // If the this.columns is already assigned before we subscribe it we will only get the new value.
        // That's why we put initial value assignment.
        // Everytime the this.columns or this.grid.columns changed we need to change the this.filterBuilder.fields, too.
        this.grid.columnsChange.pipe(takeUntil(this.ngUnsubscribe)).subscribe(p => this.setFilterBuilderFields());
    }

    ngAfterContentInit(): void {
        // Angular team recommended retrieving query results in ngAfterViewInit for view queries and ngAfterContentInit for content queries. 
        // This is because by the time those lifecycle hooks run, change detection has completed for the relevant nodes and we can guarantee 
        // that we have collected all the possible query results.

        // Some observable types will not give you its current value, like Subject type.
        // If the this.columns is already assigned before we subscribe it we will only get the new value.
        // That's why we put initial value assignment.
        // Everytime the this.columns or this.grid.columns changed we need to change the this.filterBuilder.fields, too.
        this.columns.changes.pipe(takeUntil(this.ngUnsubscribe)).subscribe(p => this.setFilterBuilderFields());
    }

    ngOnChanges(changes: SimpleChanges): void {
        // Check if current dataSource is not null.
        if (changes.dataSource && changes.dataSource.currentValue && !changes.keyName) {
            if (this.enablePaging) {
                if (changes.dataSource.currentValue.length > this.pageSize) {
                    // If current data source item count is greater than page size, then the client side paging will be enabled.
                    this.grid.dataSource = changes.dataSource.currentValue.slice(0, this.pageSize);
                    this.isClientSidePaging = true;
                    this.currentPage = 1;
                    this.paginator.gotoPage(this.currentPage);
                } else {
                    // Otherwize the server side paging is enabled.
                    this.grid.dataSource = changes.dataSource.currentValue;
                }
            } else {
                // Gets the all server data when paging is disabled.
                this.grid.dataSource = changes.dataSource.currentValue;
            }
        }
    }

    ngOnDestroy(): void {
        super.ngOnDestroy();
    }

    /**
     * Emits the pageChange event when page changes.
     * @param e: The page change event sent by the paginator component.
     */
    onPageChanged(e: any): void {
        // Calculates the paging related properties' value.
        this.pageSize = e.pageSize;
        this.currentPage = e.currentPage;
        this.totalPageCount = e.totalPageCount;

        // For client side paging.
        if (this.isClientSidePaging && this.dataSource) {
            this.grid.dataSource = this.dataSource.slice(e.pageStartIndex, e.pageEndIndex);
        }

        // Emits the pageChanged event.
        this.pageChanged.emit({
            pageStartIndex: e.pageStartIndex,
            pageEndIndex: e.pageEndIndex,
            pageSize: this.pageSize
        });
    }

    /**
     * Emits the selectionChange event when grid row selction changes.
     * @param e: The selection change event sent by the paginator component.
     */
    onSelectionChanged(e: any): void {
        // This is for clear selection button.
        this.selectedRowKeys = e.selectedRowKeys;

        // Emits the selectionChanged event. 
        this.selectionChanged.emit({
            selectedRowKeys: e.selectedRowKeys,
            selectedRowsData: e.selectedRowsData
        });
    }

    /**
     * Triggered when the data grid content is ready. The right place to apply some extra styles for data grid.
     * @param e: The content ready event sent by the data grid component.
     */
    onContentReady(e: any): void {
        this.applyColumnAllowOverflow();
    }

    /**
     * Emits the event to pass the filter expression when user clicks on the search button.
     */
    applyFilter(): void {
        // Gets the distinct column names from the filter expression.
        // Initializes the columnNames for the next recursion method.
        let columnNames = [];
        columnNames = this.getDistinctColumnNameList(columnNames, this.filterExpression);

        // Generates the column with types for backend.
        let dataColumns = this.generateDataColumns(columnNames);

        this.filterChanged.emit({
            dataColumns: dataColumns,
            filterExpression: this.filterExpression
        });
    }

    /**
     * Clears current filter expression.
     */
    clearFilter(): void {
        this.filterBuilder.writeValue(null);
    }

    /**
     * The property will be updated when the filter expression changes.
     * @param e 
     */
    filterBuilderValueChanged(e: any): void {
        this.filterExpression = e.value;
    }

    /**
     * When the fields changes it usually means switched to another data source binding.
     * Then we need to clear the filter expression.
     */
    filterBuilderFieldsChanged(): void {
        this.filterBuilder.writeValue(null);
    }

    /**
     * Initializes the class properties if necessary.
     */
    protected initializeProperties(): void {
        this.dataSource = this.dataSource || [];
        this.filterExcludedDataTypes = this.filterExcludedDataTypes || [];
        this.filterBuilderBackgroundColor = this.filterBuilderBackgroundColor || DataGridConfig.filterBuilderBackgroundColor;
        this.totalRowsCount = this.totalRowsCount || 0;
        this.keyName = this.keyName || DataGridConfig.keyName;
        this.enablePaging = this.commonService.isNullOrUndefined(this.enablePaging) ? DataGridConfig.enablePaging : this.enablePaging;
        this.allowColumnResizing = this.allowColumnResizing || DataGridConfig.allowColumnResizing;
        this.hoverStateEnabled = this.hoverStateEnabled || DataGridConfig.hoverStateEnabled;
        this.rowAlternationEnabled = this.rowAlternationEnabled || DataGridConfig.rowAlternationEnabled;
        this.enableMutipleSelection = this.enableMutipleSelection || DataGridConfig.enableMutipleSelection;
        this.pageSize = this.pageSize || DataGridConfig.pageSize;
        this.pageSizeOptions = this.pageSizeOptions || DataGridConfig.pageSizeOptions;
        this.pageLinkSize = this.pageLinkSize || DataGridConfig.pageLinkSize;
        this.columnMinWidth = DataGridConfig.columnMinWidth;
        this.needFilterBuilder = this.needFilterBuilder || DataGridConfig.needFilterBuilder;
        this.columnAutoWidth = this.columnAutoWidth || DataGridConfig.columnAutoWidth;
        this.isClientSidePaging = false;
        this.currentPage = 1;
        this.selectedRowKeys = [];
        this.totalPageCount = 0;

        // Converts the all input to upper case for later string comparison.
        this.filterExcludedDataTypes = this.filterExcludedDataTypes.map(p => p.toUpperCase());
    }

    /**
     * Applies the allowOverflow config to the data grid column.
     */
    private applyColumnAllowOverflow(): void {
        // By now this is for fixing the issue of boostrap dropdown being blocked in the DataGrid cell.

        // Because the type of this.columns is QueryList. This type does NOT have the indexOf method we are about to use.
        // That's why we do such convertion.
        let columns = this.columns.map(p => p);

        let columnsNeedsOverflow = this.columns.filter(p => p.allowOverflow);

        columnsNeedsOverflow.forEach(p => {
            // Why we need to +2 to index because this DataGrid extension reserved 2 columns for multiple selection and group expanding.  
            let number = columns.indexOf(p) + 2;

            $(`[aria-colindex="${number}"]`).css('overflow', 'visible');
        });

        if (columnsNeedsOverflow.length > 0) {
            $('.dx-scrollable-container').css('overflow', 'auto');
            $('.dx-datagrid-rowsview').css('overflow', 'visible');
        }
    }

    /**
     * Set the value of this.filterBuilder.fields based on current data grid columns.
     * The columns might be configured by html component or code.
     */
    private setFilterBuilderFields(): void {
        if (this.filterBuilder) {
            if (this.grid.columns) {
                // This is for code generated columns assigned to this.grid.columns.
                // Prepares the array for filter builder. The object contains two properties, caption and dataField.
                // We just need to filter out any item with dataField value and does not belong to filterExcludedDataTypes.
                // Because the item with cellTemplate has no dataField value and we don't need to create filter for such item.
                // Or this item's data type is in the filterExcludedDataTypes.
                this.filterBuilder.fields = this.grid.columns
                    .filter((p: any) => !this.commonService.isNullOrUndefinedOrEmptyString(p.dataField) && !this.filterExcludedDataTypes.includes(p.dataType.toUpperCase())) as any[];
            } else if (this.columns.length > 0) {
                // This is for html template component binding.
                // Prepares the array for filter builder. The object contains two properties, title and dataField.
                // We just need to filter out any item with dataField value and does not belong to filterExcludedDataTypes.
                // Because the item with cellTemplate has no dataField value and we don't need to create filter for such item.
                // Or this item's data type is in the filterExcludedDataTypes.
                // Also we need to add caption property.
                this.filterBuilder.fields = this.columns
                    .filter((p: any) => !this.commonService.isNullOrUndefinedOrEmptyString(p.dataField) && !this.filterExcludedDataTypes.includes(p.dataType.toUpperCase()))
                    .map((p: any) => {
                        p.caption = p.title;
                        return p;
                    }) as any[];
            }
        }
    }

    /**
     * This is a recursive method to get the distinct column names based on selected data grid filter expression.
     * @param columnNames An arrary contains the string of column name.
     * @param filterExpression The data grid filter expression list.
     * @returns A list contains all selected column names.
     */
    private getDistinctColumnNameList(columnNames: any[], filterExpression: any): string[] {
        // If the data grid filter exppression exists.
        if (filterExpression) {
            if (filterExpression[0] instanceof Array) {
                // If user selects multiple filters the expression would be like [["AREA_NAME", "=", "test"], "and", ["ID", "contains", "a"]]. 
                // Or even complicated with group expression. We need to go through the list and check each item.
                for (let i = 0; i < filterExpression.length; i++) {
                    let item = filterExpression[i];

                    if (item instanceof Array) {
                        this.getDistinctColumnNameList(columnNames, item);
                    }
                }
            } else {
                if (filterExpression[0] === '!') {
                    // If the expression is like ['!', ["ID", "contains", "a"]].
                    // We still need to go down to next level.
                    this.getDistinctColumnNameList(columnNames, filterExpression[1]);
                } else {
                    // If this level expression looks like ["ID", "contains", "a"].
                    // Then we only need to get the column in the first index of this expression.
                    if (!columnNames.includes(filterExpression[0])) {
                        columnNames.push(filterExpression[0]);
                    }
                }
            }
        }

        return columnNames;
    }

    /**
     * Generates the list which contains column name and column data type.
     * @param columnNames The distinct column name list.
     * @returns A list contains the instance of DataColumn.
     */
    private generateDataColumns(columnNames: string[]): DataColumn[] {
        let dataColumns = [];

        for (let i = 0; i < columnNames.length; i++) {
            // The data column info by column name.
            let dataColumn = this.dataTypeListForFilterBuilder.filter((p: DataColumn) => p.columnName === columnNames[i])[0];

            // Pushes the column data to columnTypes list.
            dataColumns.push({
                columnName: dataColumn.columnName,
                dataType: dataColumn.dataType
            });
        }

        return dataColumns;
    }

    /**
     * 将data grid本地化
     */
    private loadLocalization(): void {
        this.locale = this.getLocale();

        loadMessages(zhMessages['default']);
        locale(this.locale);
    }

    /**
     * 得到本地语言
     */
    private getLocale(): string {
        let locale = sessionStorage.getItem('locale');
        return locale != null ? locale : 'zh';
    }
}